<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Trace Gantt</title>
    <link rel="stylesheet" href="css/bootstrap.min.css">
    <link rel="stylesheet" href="css/d3-gantt.css">
    <style>
        #hourglass
        {
            display: none;
            font-size: 110%;
            color: #888;
            animation: hourglass-animation 2s linear infinite;
        }

        @keyframes hourglass-animation {
            0% { transform: none; }
            25% { transform: rotate(180deg); }
            50% { transform: rotate(180deg); }
            75% { transform: rotate(360deg); }
            100% { transform: rotate(360deg); }
        }
    </style>
</head>
<body>
    <script language="javascript" type="text/javascript" src="js/jquery.min.js"></script>
    <script language="javascript" type="text/javascript" src="js/bootstrap.min.js"></script>
    <script language="javascript" type="text/javascript" src="js/d3.v4.min.js"></script>
    <script language="javascript" type="text/javascript" src="js/d3-tip-0.8.0-alpha.1.js"></script>
    <script language="javascript" type="text/javascript" src="js/d3-gantt.js"></script>
    <script language="javascript" type="text/javascript" src="js/query-clickhouse.js"></script>

    <nav class="navbar navbar-default">
        <div class="container-fluid">
            <div class="navbar-header">
                <a class="navbar-brand" href="#">ClickHouse trace-visualizer</a>
            </div>
            <div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
                <form class="navbar-form navbar-left">
                    <button type="button" class="btn btn-primary" id="toolbar-load" data-toggle="modal" data-target="#loadModal">Load</button>
                    <button type="button" class="btn btn-primary" id="toolbar-find" data-toggle="modal" data-target="#findModal">Find</button>
                </form>
                <p class="navbar-text" id="status-text"></p>
            </div>
        </div>
    </nav>

    <div id="placeholder" class="chart-placeholder"></div>

    <div class="modal fade" id="loadModal" tabindex="-1" role="dialog" aria-labelledby="loadModalLabel">
        <div class="modal-dialog" role="document">
            <div class="modal-content">
                <div class="modal-header">
                    <button type="button" class="close" data-dismiss="modal" aria-label="Close">
                        <span aria-hidden="true">&times;</span>
                    </button>
                    <h4 class="modal-title" id="loadModalLabel">Load Data</h4>
                </div>
                <div class="modal-body">
                    <!-- Nav tabs -->
                    <ul class="nav nav-tabs" role="tablist">
                        <li class="active">
                            <a href="#tabFileUpload" role="tab" data-toggle="tab">Upload JSON</a>
                        </li>
                        <li>
                            <a href="#tabClickhouse" role="tab" data-toggle="tab">From ClickHouse</a>
                        </li>
                    </ul>
    
                    <!-- Tab panes -->
                    <div class="tab-content" style="margin-top: 15px;">
                        <!-- File Upload Tab -->
                        <div class="tab-pane active" id="tabFileUpload">
                            <input type="file" id="loadFiles" value="Load" />
                        </div>
    
                        <!-- ClickHouse Query Tab -->
                        <div class="tab-pane" id="tabClickhouse">
                            <form id="params">
                                <div id="connection-params" style="margin-bottom: 10px;">
                                    <input spellcheck="false" id="url" type="text" value="http://localhost:8123" placeholder="URL" class="form-control" style="margin-bottom: 5px;" />
                                    <input spellcheck="false" id="user" type="text" value="" placeholder="user" class="form-control" style="margin-bottom: 5px;" />
                                    <input spellcheck="false" id="password" type="password" placeholder="password" value="" class="form-control" style="margin-bottom: 5px;" />
                                </div>
                                <textarea spellcheck="false" id="query" rows="26" class="form-control" placeholder="SQL Query">WITH '__PUT_YOUR_QUERY_ID_HERE__' AS my_query_id
SELECT
    ('thread #' || leftPad(attribute['clickhouse.thread_id'], 6, '0')) AS group,
    replaceRegexpOne(operation_name, '(.*)_.*', '\\1') AS operation_name,
    start_time_us,
    finish_time_us,
    sipHash64(operation_name) AS color,
    attribute
FROM system.opentelemetry_span_log
WHERE 1
    AND trace_id IN (
        SELECT trace_id
        FROM system.opentelemetry_span_log
        WHERE (attribute['clickhouse.query_id']) IN (SELECT query_id FROM system.query_log WHERE initial_query_id = my_query_id)
    )
    AND operation_name !='query'
    AND operation_name NOT LIKE '%Pipeline%'
    AND operation_name NOT LIKE 'TCPHandler%'
    AND operation_name NOT LIKE 'Query%'
ORDER BY
    group ASC,
    parent_span_id ASC,
    start_time_us ASC
SETTINGS output_format_json_named_tuples_as_objects = 1, skip_unavailable_shards = 1</textarea>
                                <div class="checkbox" style="margin-top: 10px;">
                                    <label>
                                        <input type="checkbox" id="flushLogsCheckbox" checked> Run SYSTEM FLUSH LOGS
                                    </label>
                                </div>
                                <div id="query-error" style="color: red; margin-top: 10px; display: none;"></div>
                            </form>
                        </div>
                    </div>
                </div>
    
                <div class="modal-footer">
                    <button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
                    <button type="button" class="btn btn-primary" id="btnDoLoad">Load</button>
                    <button type="button" class="btn btn-success" id="btnQueryClickhouse" style="display: none;">Query</button>
                    <div id="hourglass">⧗</div>
                </div>
            </div>
        </div>
    </div>

    <div class="modal fade" id="findModal" tabindex="-1" role="dialog" aria-labelledby="findModalLabel">
        <div class="modal-dialog" role="document">
            <div class="modal-content">
                <div class="modal-header">
                    <button type="button" class="close" data-dismiss="modal" aria-label="Close"><span
                            aria-hidden="true">&times;</span></button>
                    <h4 class="modal-title" id="findModalLabel">Span Filter</h4>
                </div>
                <div class="modal-body">
                    <input type="text" id="filterPattern" value="" class="form-control"/>
                </div>
                <div class="modal-footer">
                    <button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
                    <button type="button" class="btn btn-primary" id="btnDoFindClear">Clear</button>
                    <button type="button" class="btn btn-primary" id="btnDoFind">Find</button>
                </div>
            </div>
        </div>
    </div>
</body>
</html>

<script language="javascript">
    var example_json = [];
    var example_colors = ["#f88", "#8f8", "#88f", "#888", "#aaa"];
    for (let i = 0; i < 1024; i++) {
        example_json.push({
            t1: i + 50 - Math.random() * 60,
            t2: i + 50 + Math.random() * 60,
            band: "band" + (i % 128),
            color: example_colors[i % example_colors.length],
            text: "it is span #" + i
        });
    }
    example_json.bands = new Set(example_json.map(x => x.band));
    example_json.max_time_ms = Math.max(...example_json.map(x => x.t2));

    var data = null;
    var chart = null;

    function updateStatus(pattern = '') {
        if (pattern != '') {
            const runtime = chart.filteredRuntime();
            const total = chart.totalRuntime();
            $("#status-text").text(`
                ${chart.filteredCount()}/${chart.totalCount()} spans
                ${runtime.toFixed(1)}/${total.toFixed(1)} runtime (${(runtime*100/total).toFixed(1)}%)
                matching
            `);
        }
        else
        {
            $("#status-text").text(`
                ${chart.totalCount()} spans
                ${chart.totalRuntime().toFixed(1)} runtime
                total
            `);
        }
    }

    function renderChart(parsed) {
        data = parsed;
        if (chart != null) {
            chart.destroy();
        }
        let view_height = window.innerHeight - $("#placeholder")[0].getBoundingClientRect().y - 40;
        let data_height = parsed.bands.size * 8;
        chart = d3.gantt()
            .height(Math.max(view_height, data_height))
            .view_height(view_height)
            .selector("#placeholder")
            .timeDomain([0, parsed.max_time_ms])
        ;
        chart(data);
        updateStatus();
    }

    $("<div id='errmsg'></div>").css({
        position: "absolute",
        display: "none",
        border: "1px solid #faa",
        padding: "2px",
        "background-color": "#fcc",
        opacity: 0.80
    }).appendTo("body");

    function fetchData(dataurl, parser = x => x) {
        function onDataReceived(json, textStatus, xhr) {
            $("#errmsg").hide();
            renderChart(parser(json));
        }

        function onDataError(xhr, error) {
            console.log(arguments);
            $("#errmsg").text("Fetch data error: " + error + (xhr.status == 200? xhr.responseText: ""))
            .css({bottom: "5px", left: "25%", width: "50%"})
            .fadeIn(200);
        }

        if (dataurl) {
            $.ajax({
                url: dataurl,
                type: "GET",
                dataType: "json",
                success: function (json, textStatus, xhr) { onDataReceived(json, textStatus, xhr); },
                error: onDataError
            });
        } else {
            onDataReceived(example_json, "textStatus", "xhr");
        }
    }

    // Show correct button depending on tab
    $('#loadModal').on('shown.bs.modal', function () {
        $('a[data-toggle="tab"]').on('shown.bs.tab', function (e) {
            if ($(e.target).attr('href') === '#tabClickhouse') {
                $('#btnDoLoad').hide();
                $('#btnQueryClickhouse').show();
            } else {
                $('#btnDoLoad').show();
                $('#btnQueryClickhouse').hide();
            }
        });
    });

    // Handle ClickHouse query
    let query_controller = null;
    $('#btnQueryClickhouse').on('click', async function () {
        if (query_controller == null) {
            let rows = [];
            let error = '';
            const errorDiv = document.getElementById('query-error');
            errorDiv.style.display = 'none';
            errorDiv.textContent = '';
            document.getElementById('hourglass').style.display = 'inline-block';
            $('#btnQueryClickhouse').text("Stop");
            if ($('#flushLogsCheckbox').is(':checked')) {
                query_controller = new AbortController();
                await queryClickHouse({
                    host: $('#url').val(),
                    user: $('#user').val(),
                    password: $('#password').val(),
                    query: "SYSTEM FLUSH LOGS",
                    for_each_row: (data) => {},
                    on_error: (errorMsg) => error = errorMsg,
                    controller: query_controller
                });
            }
            if (error == '') {
                query_controller = new AbortController();
                await queryClickHouse({
                    host: $('#url').val(),
                    user: $('#user').val(),
                    password: $('#password').val(),
                    query: $('#query').val(),
                    for_each_row: (data) => rows.push(data),
                    on_error: (errorMsg) => error = errorMsg,
                    controller: query_controller
                });
            }
            query_controller = null;
            $('#btnQueryClickhouse').text("Query");
            document.getElementById('hourglass').style.display = 'none';

            if (error != '') {
                errorDiv.textContent = error;
                errorDiv.style.display = 'block';
            } else {
                renderChart(parseClickHouseTrace(rows));
                $('#loadModal').modal('hide');
            }
        } else { // Cancel query
            query_controller.abort();
        }
    });

    // Handle uploaded JSON
    $("#btnDoLoad").click(function(){
        let element = document.getElementById('loadFiles');
        let files = element.files;
        if (files.length <= 0) {
            return false;
        }
        let fr = new FileReader();
        fr.onload = function(e) {
            $("#errmsg").hide();
            renderChart(parseClickHouseTrace(JSON.parse(e.target.result).data));
        }
        fr.readAsText(files.item(0));
        element.value = '';
        $('#loadModal').modal('hide');
    });

    function doFindPattern() {
        if (chart) {
            const pattern = document.getElementById('filterPattern').value;
            chart.filter(pattern);
            updateStatus(pattern);
        }
        $('#findModal').modal('hide');
    }

    $("#btnDoFindClear").click(function(){
        document.getElementById('filterPattern').value = '';
        doFindPattern();
    });

    $("#btnDoFind").click(function(){
        doFindPattern();
    });

    function parseClickHouseTrace(rows) {
        let min_time_us = Number.MAX_VALUE;
        for (let i = 0; i < rows.length; i++) {
            let span = rows[i];
            min_time_us = Math.min(min_time_us, +span.start_time_us);
        }

        let max_time_ms = 0;
        function convertTime(us) {
            let value = (us - min_time_us) / 1000;
            max_time_ms = Math.max(max_time_ms, value);
            return value;
        }

        function strHash(str) {
            var hash = 0;
            if (str.length === 0)
                return hash;
            for (let i = 0; i < str.length; i++) {
                hash = ((hash << 5) - hash) + str.charCodeAt(i);
                hash |= 0; // Convert to 32bit integer
            }
            if (hash < 0)
                hash = -hash;
            return hash;
        }

        let result = [];
        let bands = new Set();
        for (let i = 0; i < rows.length; i++) {
            let span = rows[i];
            let band = Object.values(span.group).join(' ');
            bands.add(band);
            result.push({
                t1: convertTime(+span.start_time_us),
                t2: convertTime(+span.finish_time_us),
                band,
                color: d3.interpolateRainbow((strHash(span.color) % 1024) / 1024),
                text: span.operation_name
            });
        }

        result.bands = bands;
        result.max_time_ms = max_time_ms;
        return result;
    }

    fetchData(); // do not fetch, just draw example_json w/o parsing
    //fetchData("your-traces.json", parseClickHouseTrace);
</script>
